﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GurLoader.Preferences;
using Moq;
using NUnit.Framework;

namespace GurLoaderWPF.Test.Preferences
{
    [TestFixture]
    public class ApplicationPreferencesBaseTest
    {
        private class TestPreferences : ApplicationPreferencesBase<TestPreferences>
        {
            public TestPreferences()
            {
            }

            public TestPreferences(string fileName) : base(fileName)
            {
            }

            public TestPreferences(string fileName, IFileOperations fileOperations) : base(fileName, fileOperations)
            {
            }

            public string Value1 { get; set; }
            public int Value2 { get; set; }

            protected bool Equals(TestPreferences other)
            {
                return string.Equals(Value1, other.Value1) && Value2 == other.Value2;
            }

            public override bool Equals(object obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                if (obj.GetType() != this.GetType()) return false;
                return Equals((TestPreferences) obj);
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return ((Value1 != null ? Value1.GetHashCode() : 0)*397) ^ Value2;
                }
            }

            public static bool operator ==(TestPreferences left, TestPreferences right)
            {
                return Equals(left, right);
            }

            public static bool operator !=(TestPreferences left, TestPreferences right)
            {
                return !Equals(left, right);
            }
        }

        [Test]
        public void CreatePreferencesWithDefaultFilename()
        {
            TestPreferences prefs = new TestPreferences();
            Assert.IsNotNull(prefs);
        }
        
        [Test]
        public void CreatePreferencesWithCustomFilename()
        {
            const string testPrefsFileName = @"C:\Path\to\prefs.json";

            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();
            TestPreferences prefs =
                new TestPreferences(testPrefsFileName, mockFileOps.Object);

            Assert.IsNotNull(prefs);
            Assert.AreEqual(testPrefsFileName, prefs.FileName);
        }

        [Test]
        [ExpectedException(typeof(ArgumentException), ExpectedMessage = "Filename must not be null.")]
        public void CreatePreferencesNullFilenameTest()
        {
            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();
            TestPreferences prefs =
                new TestPreferences(null, mockFileOps.Object);
        }

        [Test]
        [ExpectedException(typeof(ArgumentException), ExpectedMessage = "File operations must not be null.")]
        public void CreatePreferencesNullFileOpsTest()
        {
            const string testPrefsFileName = @"C:\Path\to\prefs.json";

            TestPreferences prefs =
                new TestPreferences(testPrefsFileName, null);
        }

        [Test]
        public void SaveTest()
        {
            const string testPrefsFileName = @"C:\Path\to\prefs.json";
            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();
            
            TestPreferences prefs =
                new TestPreferences(testPrefsFileName, mockFileOps.Object);

            Assert.IsNotNull(prefs);
            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            prefs.Value1 = testValue1;
            prefs.Value2 = testValue2;

            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            mockFileOps.Setup(x => x.WriteAllText(testPrefsFileName, testJsonData)).Verifiable();
            
            prefs.Save();

            mockFileOps.VerifyAll();
        }

        [Test]
        public void PreferencesSavedEventTest()
        {
            const string testPrefsFileName = @"C:\Path\to\prefs.json";
            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();

            TestPreferences prefs =
                new TestPreferences(testPrefsFileName, mockFileOps.Object);

            Assert.IsNotNull(prefs);
            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            prefs.Value1 = testValue1;
            prefs.Value2 = testValue2;

            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            mockFileOps.Setup(x => x.WriteAllText(testPrefsFileName, testJsonData)).Verifiable();

            ManualResetEvent mre = new ManualResetEvent(false);

            prefs.PreferencesSaved += (sender, e) => mre.Set();

            prefs.Save();

            bool eventSet = mre.WaitOne(1000*3);

            if (! eventSet)
                Assert.Fail("Timeout waiting for event callback.");

            mockFileOps.VerifyAll();
        }

        [Test]
        public void SaveToCustomFileTest()
        {
            const string testPrefsFileName = @"C:\Path\to\prefs.json";
            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();

            TestPreferences prefs =
                new TestPreferences(testPrefsFileName, mockFileOps.Object);

            Assert.IsNotNull(prefs);
            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            prefs.Value1 = testValue1;
            prefs.Value2 = testValue2;

            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            const string alternatePrefsFileName = @"C:\Other\Path\to\Prefs.json";

            mockFileOps.Setup(x => x.WriteAllText(alternatePrefsFileName, testJsonData)).Verifiable();

            prefs.Save(alternatePrefsFileName);

            mockFileOps.VerifyAll();
        }

        [Test]
        public void StaticSaveTest()
        {
            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();

            TestPreferences prefs = new TestPreferences();

            Assert.IsNotNull(prefs);
            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            prefs.Value1 = testValue1;
            prefs.Value2 = testValue2;

            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            mockFileOps.Setup(x => x.WriteAllText(prefs.FileName, testJsonData)).Verifiable();

            TestPreferences.Save( prefs, mockFileOps.Object );

            mockFileOps.VerifyAll();
        }

        [Test]
        public void StaticSaveWithCallbackTest()
        {
            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();

            TestPreferences prefs = new TestPreferences();

            Assert.IsNotNull(prefs);
            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            prefs.Value1 = testValue1;
            prefs.Value2 = testValue2;

            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            mockFileOps.Setup(x => x.WriteAllText(prefs.FileName, testJsonData)).Verifiable();

            ManualResetEvent mre = new ManualResetEvent(false);

            TestPreferences.Save(prefs, mockFileOps.Object, () => mre.Set());

            bool mreSet = mre.WaitOne(1000*3);

            if (! mreSet)
                Assert.Fail("Timeout waiting for callback.");

            mockFileOps.VerifyAll();
        }

        [Test]
        public void StaticSaveWithCustomFilenameTest()
        {
            const string testPrefsFileName = @"C:\Path\to\prefs.json";
            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();

            TestPreferences prefs = new TestPreferences();

            Assert.IsNotNull(prefs);
            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            prefs.Value1 = testValue1;
            prefs.Value2 = testValue2;

            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            const string alternatePrefsFileName = @"C:\Other\Path\to\Prefs.json";

            mockFileOps.Setup(x => x.WriteAllText(alternatePrefsFileName, testJsonData)).Verifiable();

            TestPreferences.Save(prefs, alternatePrefsFileName, mockFileOps.Object);

            mockFileOps.VerifyAll();
        }

        [Test]
        public void StaticLoadTest()
        {
            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();
            
            mockFileOps.Setup(x=>x.FileExists(It.IsAny<string>())).Returns(true).Verifiable();
            mockFileOps.Setup(x=>x.ReadAllText( It.IsAny<string>() ) ).Returns(testJsonData).Verifiable();

            TestPreferences prefs = TestPreferences.Load(mockFileOps.Object);

            Assert.IsNotNull(prefs);
            
            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            Assert.AreEqual(testValue1, prefs.Value1);
            Assert.AreEqual(testValue2, prefs.Value2);

            mockFileOps.VerifyAll();
        }

        [Test]
        public void StaticLoadFromCustomFilenameTest()
        {
            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();

            const string alternatePrefsFileName = @"C:\Other\Path\to\Prefs.json";

            mockFileOps.Setup(x => x.FileExists(alternatePrefsFileName)).Returns(true).Verifiable();
            mockFileOps.Setup(x => x.ReadAllText(alternatePrefsFileName)).Returns(testJsonData).Verifiable();

            TestPreferences prefs = TestPreferences.Load(alternatePrefsFileName, mockFileOps.Object);

            Assert.IsNotNull(prefs);

            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            Assert.AreEqual(testValue1, prefs.Value1);
            Assert.AreEqual(testValue2, prefs.Value2);

            mockFileOps.VerifyAll();
        }

        [Test]
        public void BeginEditTest()
        {
            const string testPrefsFileName = @"C:\Path\to\prefs.json";

            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();
            TestPreferences prefs = new TestPreferences(testPrefsFileName, mockFileOps.Object);

            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            prefs.Value1 = testValue1;
            prefs.Value2 = testValue2;

            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            const string testTempFileName = @"C:\temp\backup.json";

            mockFileOps.Setup(x=>x.GetTempFileName()).Returns(testTempFileName).Verifiable();

            // should write the json data to the temp file
            mockFileOps.Setup(x=>x.WriteAllText(testTempFileName, testJsonData)).Verifiable();

            prefs.BeginEdit();

            mockFileOps.VerifyAll();
        }

        [Test]
        public void MultipleBeginEditCallsTest()
        {
            const string testPrefsFileName = @"C:\Path\to\prefs.json";

            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();
            TestPreferences prefs = new TestPreferences(testPrefsFileName, mockFileOps.Object);

            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            prefs.Value1 = testValue1;
            prefs.Value2 = testValue2;

            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            const string testTempFileName = @"C:\temp\backup.json";

            mockFileOps.Setup(x => x.GetTempFileName()).Returns(testTempFileName).Verifiable();

            // should write the json data to the temp file
            mockFileOps.Setup(x => x.WriteAllText(testTempFileName, testJsonData)).Verifiable();

            // multiple calls should do nothing after the first
            prefs.BeginEdit();
            prefs.BeginEdit();
            prefs.BeginEdit();

            mockFileOps.VerifyAll();
        }

        [Test]
        public void EndEditTest()
        {
            const string testPrefsFileName = @"C:\Path\to\prefs.json";

            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();
            TestPreferences prefs = new TestPreferences(testPrefsFileName, mockFileOps.Object);
            TestPreferences comparablePrefs = new TestPreferences(testPrefsFileName, mockFileOps.Object);

            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            prefs.Value1 = testValue1;
            prefs.Value2 = testValue2;
            comparablePrefs.Value1 = testValue1;
            comparablePrefs.Value2 = testValue2;

            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            const string testTempFileName = @"C:\temp\backup.json";

            mockFileOps.Setup(x => x.GetTempFileName()).Returns(testTempFileName).Verifiable();

            // should write the json data to the temp file
            mockFileOps.Setup(x => x.WriteAllText(testTempFileName, testJsonData)).Verifiable();

            prefs.BeginEdit();
            // expect to delete the backup file
            mockFileOps.Setup(x=>x.FileExists(It.IsAny<string>())).Returns(true).Verifiable();
            mockFileOps.Setup(x=>x.DeleteFile(It.IsAny<string>())).Verifiable();

            const string testValue1New = "NewTestValue1";
            const int testValue2New = 12;

            prefs.Value1 = testValue1New;
            prefs.Value2 = testValue2New;

            prefs.EndEdit();

            Assert.AreNotEqual(comparablePrefs, prefs);

            mockFileOps.VerifyAll();
        }

        [Test]
        public void CancelEditTest()
        {
            const string testPrefsFileName = @"C:\Path\to\prefs.json";

            Mock<IFileOperations> mockFileOps = new Mock<IFileOperations>();
            TestPreferences prefs = new TestPreferences(testPrefsFileName, mockFileOps.Object);
            TestPreferences comparablePrefs = new TestPreferences(testPrefsFileName, mockFileOps.Object);

            const string testValue1 = "TestValue1";
            const int testValue2 = 42;

            prefs.Value1 = testValue1;
            prefs.Value2 = testValue2;
            comparablePrefs.Value1 = testValue1;
            comparablePrefs.Value2 = testValue2;

            const string testJsonData =
                "{\"Value1\":\"TestValue1\",\"Value2\":42}";

            const string testTempFileName = @"C:\temp\backup.json";

            mockFileOps.Setup(x => x.GetTempFileName()).Returns(testTempFileName).Verifiable();

            // should write the json data to the temp file
            mockFileOps.Setup(x => x.WriteAllText(testTempFileName, testJsonData)).Verifiable();

            prefs.BeginEdit();

            mockFileOps.Setup(x => x.FileExists(testTempFileName)).Returns(true).Verifiable();
            mockFileOps.Setup(x => x.ReadAllText(testTempFileName)).Returns(testJsonData).Verifiable();

            mockFileOps.Setup(x => x.FileExists(testTempFileName)).Returns(true).Verifiable();
            mockFileOps.Setup(x => x.DeleteFile(testTempFileName)).Verifiable();

            const string testValue1New = "NewTestValue1";
            const int testValue2New = 12;

            prefs.Value1 = testValue1New;
            prefs.Value2 = testValue2New;

            // before cancel they are not equal
            Assert.AreNotEqual(comparablePrefs, prefs);

            prefs.CancelEdit();

            // after cancel they are equal
            Assert.AreEqual(comparablePrefs, prefs);

            mockFileOps.VerifyAll();
        }
    }
}
